类与对象(下)

1. 再谈构造函数

在面向对象编程中,构造函数 是一种特殊的成员函数,它在对象创建时自动调用,负责初始化对象的成员变量(创建对象时赋初值),确保对象在创建时有一个有效的状态。接下来,我们将详细讲解关于构造函数的几个重要概念。

1.1 构造函数体赋值

当我们创建一个对象时,构造函数会被自动调用,用来给对象的各个成员变量提供一个初始值。例如:

1
2
3
4
5
6
7
8
9
class MyClass
{
public:
int a;
MyClass()
{
a = 10; // 这里是构造函数体中的赋值操作
}
};

在这个例子中,构造函数的作用是将 a 赋值为 10。然而,这里要注意,构造函数体中的赋值操作和初始化是有区别的。构造函数体中对成员变量的赋值只能算是给成员变量“赋初值”,而不是“初始化”。因为初始化是指给成员变量设置一个初始值,而且初始化只能发生一次,而赋值操作可以发生多次。

再例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
class A
{
int _x;
public:
A(int x)
{
_x = x; // 赋值操作:_x 在此之前已经默认初始化(如果有的话),然后被赋值为 x。
}
};

上述代码中,`_x = x;`是赋值,而非初始化,因为初始化只能通过构造函数外部的初始化列表来完成。

在构造函数体中对成员变量的赋值操作,实际上是先通过默认初始化(如果有的话),然后通过赋值操作覆盖初始值。这与初始化列表直接初始化成员变量有本质区别。

换句话说,初始化只能在构造函数中通过初始化列表来进行一次,而构造函数体中的赋值可以反复进行

所以,赋值与初始化的区别 在于:

  • 初始化:是在对象创建时为成员变量设置初始值的过程,每个成员变量只能被初始化一次,通常在构造函数的初始化列表中进行(只能进行一次)。
  • 赋值:是在对象创建后对成员变量进行值的修改,可以发生多次,可以在构造函数体中,也可以在对象的生命周期中的任何时候进行(可以重复进行)。

1.2 初始化列表

初始化列表是构造函数中的一种写法(直接初始化类的成员变量的一种方式),允许我们在构造函数调用之前就给成员变量提供初始值。语法是在构造函数的括号后加冒号和成员变量列表,其基本格式是:

1
2
3
4
5
6
MyClass() : a(10)
{
// 构造函数体
}

这里的:`a(10)`就是初始化列表,它会在构造函数体执行之前初始化成员变量`a`为10

再或者:

1
2
3
4
5
6
7
8
9
10
11
12
class A
{
int _x;
int _y;
int _val;
public:
A(int x)
: _x(x),
_y(0),
_val(0)
{} // 初始化列表
};

注意事项

  1. 每个成员变量在初始化列表中只能出现一次。初始化列表的目的是对成员进行一次初始化。
  2. 对于某些特殊类型的成员变量,必须使用初始化列表来进行初始化。例如:
    • 引用成员变量:引用一旦初始化就不可更改,只能在初始化列表中赋值。
    • const 成员变量:常量变量(const 变量)在对象创建时必须初始化,且不可更改;必须在声明时初始化,不能通过构造函数体赋值。
    • 自定义类型成员/没有默认构造函数的自定义类型:自定义类型变量如果没有默认构造函数,只能通过初始化列表初始化。
  3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
  4. 初始化列表的顺序:初始化列表中的成员变量的初始化顺序 和它们在类中声明的顺序一致,而不是按照初始化列表中出现的顺序(编译器会按照成员变量在类中声明的顺序进行内存布局,因此初始化顺序也必须与之匹配)。

推荐使用初始化列表的原因: 对于自定义类型,编译器会自动调用其构造函数完成初始化,不使用初始化列表而采用先默认构造再赋值可能导致性能浪费。

类的成员变量初始化顺序 取决于 成员变量的声明顺序,而不是初始化列表的顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

class MyClass
{
public:
// 此处先声明 a, 再声明 b
int a;
const int b;

MyClass(int x) : b(20), a(x) // 初始化列表:b 在 a 之前初始化(就会导致一些奇怪的错误:未定义行为(UB))
{
// b 在 a 之后声明,所以 a 会先被初始化,b 才会初始化!
// 如果 b 的初始化依赖 a,就会导致意外的错误或 UB!
cout << "a = " << a << ", b = " << b << endl;
}
};

int main()
{
MyClass obj(10); // 输出:a = 10, b = 20
return 0;
}

1.3 explicit 关键字

如果构造函数接收单一参数,它可能会被编译器用于隐式类型转换。而 explicit 关键字则用来标记构造函数,防止它参与隐式类型转换。 隐式类型转换可能导致临时对象的创建和销毁,增加不必要的性能开销,或者在代码中引入难以察觉的错误。

在 C++中,构造函数不仅可以用来构造和初始化对象,还可以在特定条件下用作类型转换的“隐式转换构造函数”。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class MyClass
{
public:
MyClass(int x)
{
// 构造函数
}
};

// error: conversion from 'int' to non-scalar type 'MyClass' requested
MyClass obj = 10; // 编译器会用构造函数将10转换为MyClass对象

在上述代码中,`MyClass(int x)`构造函数是一个单参构造函数,可以将`int`类型的10隐式转换为`MyClass`类型的对象。
为防止这种类型转换,我们可以在构造函数前加上`explicit`关键字,这样就禁止了隐式转换:

class MyClass
{
public:
explicit MyClass(int x)
{
// 构造函数
}
};

MyClass obj = 10; // 错误:不能隐式转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A
{
public:
A(int x) {} // 单参构造函数
};

A obj = 10; // 隐式调用构造函数

**explicit的作用**: 使用`explicit`关键字禁止隐式类型转换:

class A
{
public:
explicit A(int x) {}
};

A obj = 10; // 错误,禁止隐式转换
A obj2(10); // 正确
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;

class MyClass
{
public:
MyClass(int x) {}
};

class MyExplicitClass
{
public:
explicit MyExplicitClass(int x) {}
};

int main()
{
MyClass obj1 = 10; // 隐式类型转换
MyExplicitClass obj2 = 10; // 错误:禁止隐式类型转换
MyExplicitClass obj3(10); // 显式类型转换

cout << "obj1 created successfully." << endl;
// cout << "obj2 created successfully." << endl; // 编译错误
cout << "obj3 created successfully." << endl;

return 0;
}

使用 explicit 关键字后,只有明确调用构造函数时才能进行类型转换,不能再进行隐式转换。所以,构造函数不仅仅负责创建对象,它还通过初始化列表给对象成员变量赋初值。通过 explicit 关键字,我们可以禁止单参构造函数的隐式类型转换,确保代码的类型安全。


2. static 成员

2.1 概念

在 C++中,类成员可以被声明为 static,这样的成员称为 静态成员。静态成员包括静态成员变量和静态成员函数。

  • 静态成员变量:使用 static 修饰的类成员变量,所有对象共享这个变量,不属于某个特定的对象。静态成员变量必须在类外进行初始化(被修饰的变量属于类本身,而不是某个对象)。
  • 静态成员函数:使用 static 修饰的成员函数,它是属于类的,而不是类的某个对象(修饰的函数属于类本身,可以直接通过类名调用)。

2.2 特性

  1. 静态成员的共享性: 静态成员变量为所有对象共享,修改其中一个对象的静态成员变量值,会影响其他对象。

    • 静态成员变量和静态成员函数是属于类本身的,而不是类的某个特定对象。
    • 所有类对象共享静态成员,这意味着静态成员的值对所有对象是相同的。
    • 静态成员并不存储在每个对象中,而是存放在一个全局的静态区。

    例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class A
    {
    static int _count;
    public:
    static void Increment() { _count++; }
    static int GetCount() { return _count; }
    };

    int A::_count = 0;// 静态变量必须在类外定义/初始化

    int main()
    {
    A::Increment();
    cout << A::GetCount() << endl; // 输出1
    }
  2. 静态成员变量的初始化

    • 静态成员变量在类外进行定义和初始化,而在类内部仅声明。

    • 例如:

      1
      2
      3
      4
      5
      6
      7
      class MyClass
      {
      public:
      static int count; // 静态成员变量声明
      };

      int MyClass::count = 0; // 静态成员变量初始化
  3. 访问静态成员

    • 可以通过类名直接访问静态成员:类名::静态成员
    • 也可以通过对象访问静态成员:对象.静态成员。但是,通常推荐通过类名来访问,保持代码的清晰和一致性。
  4. 静态成员函数没有 this 指针

    • 静态成员函数与类的对象无关,静态成员函数属于类本身,而不是某个对象,因此它没有隐藏的 this 指针。
    • 由于没有 this 指针,静态成员函数无法访问类的非静态成员变量或非静态成员函数(只能访问静态变量,不能访问非静态成员)。
    • 非静态成员属于对象,而静态成员函数与对象无关,因此无法通过 this 指针访问非静态成员。
  5. 访问权限

    • 静态成员仍然受类的访问控制(publicprotectedprivate)的限制。

例子:

假设我们要实现一个类,统计类的对象创建次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;

class MyClass
{
public:
static int count; // 静态成员变量,属于类本身

MyClass()
{
count++; // 每创建一个对象,静态变量 count 加 1
}

static int getCount()
{
return count; // 静态成员函数,返回静态变量的值
}
};

int MyClass::count = 0; // 类外初始化静态成员变量

int main()
{
MyClass obj1, obj2, obj3;
cout << "创建的对象数:" << MyClass::getCount() << endl; // 输出:创建的对象数:3
return 0;
}

静态成员函数和非静态成员函数的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
using namespace std;

class MyClass
{
public:
static int staticVar;
int nonStaticVar;

static void staticFunction()
{
cout << "static 函数,staticVar =" << staticVar << endl;
// cout << "非静态 var =" << nonStaticVar << endl; // 编译错误
}

void nonStaticFunction()
{
cout << "非静态函数,nonStaticVar =" << nonStaticVar << endl;
cout << "静态 var =" << staticVar << endl;
}
};

int MyClass::staticVar = 10;

int main()
{
MyClass::staticFunction(); // 静态函数调用
MyClass obj;
obj.nonStaticFunction(); // 非静态函数调用

return 0;
}

传道解惑:

Q1:静态成员函数可以调用非静态成员函数吗?

不可以。静态成员函数没有 this 指针,它与对象无关。因此,它无法访问类的非静态成员变量和非静态成员函数。如果静态成员函数试图访问非静态成员函数或成员变量,会导致编译错误。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>class MyClass
>{
>public:
void nonStaticFunction()
{
cout << "这是一个非静态函数。" << endl;
}

static void staticFunction()
{
// 不能访问非静态成员函数
// nonStaticFunction(); // 错误:静态成员函数不能访问非静态成员函数
}
>};

Q2: 非静态成员函数可以调用类的静态成员函数吗?

可以。非静态成员函数属于某个对象,可以访问类的静态成员变量和静态成员函数。非静态成员函数是通过对象的 this 指针来访问类的成员的,所以它可以直接访问静态成员。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
>class MyClass
>{
>public:
static int staticVar;
int nonStaticVar;

void nonStaticFunction()
{
// 可以访问静态成员函数
staticFunction();
cout << "静态变量:" << staticVar << endl;
}

static void staticFunction()
{
cout << "这是一个静态函数。" << endl;
}
>};

>int MyClass::staticVar = 10;

>int main()
>{
MyClass obj;
obj.nonStaticFunction(); // 调用非静态成员函数,内部可以访问静态成员函数
return 0;
>}

输出:

1
2
>这是一个静态函数。
>静态变量:10

3. 友元(Friend)

友元 是一种打破封装的方式,它允许特定的函数或类访问类的私有成员和保护成员。通常情况下,类的私有成员是只能通过类的成员函数来访问的,但友元提供了一个特例,使得外部函数或类可以访问这些私有成员。尽管友元能提供便利,但它也会增加类之间的耦合度,破坏封装性,因此在设计时应当小心使用。

友元可以分为两种类型:友元函数友元类

3.1 友元函数

  • 友元函数的定义: 友元函数是定义在类外的普通函数,但可以访问类的私有成员和保护成员。它并不是类的成员函数,因此没有 this 指针。虽然它是类外的函数,但为了让它可以访问类的私有成员,必须在类内声明为友元函数,使用 friend 关键字。

  • 友元函数的特性:

    • 声明位置:可以在类定义的任何地方声明,只要在类内部用 friend 关键字。
    • 访问权限:友元函数可以访问类的私有和保护成员,但它并不属于类的成员函数。
    • 不属于类的成员函数:友元函数的调用和普通函数一样,不通过对象调用,没有 this 指针。
    • 不能用 const 修饰:友元函数不能被声明为 const,因为它可能会修改类的私有成员。

    例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    #include <iostream>
    using namespace std;

    class A; // 前向声明类 A

    class B
    {
    int _y;
    public:
    B(int y) : _y(y) {}
    friend class A; // 友元类:A 类可以访问 B 的私有成员
    };

    class A
    {
    int _x;
    public:
    A(int x) : _x(x) {}

    friend void Print(const A& a); // 友元函数声明

    // 友元类 A 可以访问 B 的私有成员
    void Print(const B& b)
    {
    cout << "Friend class: " << b._y << endl; // 访问 B 的私有成员
    }
    };

    void Print(const A& a)
    {
    cout << "Friend function: " << a._x << endl; // 访问 A 的私有成员
    }

    int main()
    {
    A a(10);
    Print(a); // 调用友元函数

    B b(20);
    a.Print(b); // 调用友元类中的成员函数

    return 0;
    }
  • 友元函数的使用场景

    • 当需要定义一个全局函数,并且需要访问类的私有数据时。
    • 常用于重载操作符 <<>>

传道解惑

Q1:为什么使用友元函数?

友元函数常用于那些不能作为成员函数实现的操作。例如,重载输入输出运算符 <<>> 时,coutcin 对象是流对象,需要通过全局函数进行操作,而不能作为类的成员函数。

示例:重载 operator<<(输出流)为友元函数

假设我们有一个 Box 类,想要重载 << 运算符来输出 Box 的内容,cout 需要是左操作数,因此 << 不能作为成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
>#include <iostream>
>using namespace std;

>class Box
>{
>private:
int length;
public:
Box(int l) : length(l) {}

// 声明友元函数
friend ostream& operator<<(ostream& os, const Box& box);
>};

>// 友元函数的定义
>ostream& operator<<(ostream& os, const Box& box)
>{
os << "箱长:" << box.length;
return os;
>}

>int main()
>{
Box box(10);
cout << box << endl; // 使用重载的operator<<
return 0;
>}

输出:

1
>箱长:10

在这个例子中,operator<< 是一个友元函数,它被声明为 Box 类的友元,因此它可以访问 Box 类的私有成员 length


3.2 友元类

友元类 是指一个类的所有成员函数都可以成为另一个类的友元函数,因此友元类的所有成员函数都可以访问当前类的私有成员和保护成员。

友元类的特性:

  • 单向关系:如果类 A 声明类 B 为友元类,那么类 B 的成员函数可以访问类 A 的私有成员,但类 A 的成员函数不能访问类 B 的私有成员。友元关系是单向的(声明友元类 B 后,B 能访问 A,但 A 不能访问 B)。
  • 不能继承:友元关系不能被继承,如果类 A 是类 B 的友元类,那么类 B 的派生类不会自动成为类 A 的友元类。
  • 关系不可传递:如果 B 是 A 的友元,C 是 B 的友元,那么 C 并不是 A 的友元。

示例:友元类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A
{
int _x;
public:
A(int x) : _x(x) {}
friend class B; // 声明B为友元类
};

class B
{
public:
void AccessA(const A& a)
{
cout << a._x << endl; // 访问A的私有成员
}
};

再假设我们有 Time 类和 Date 类,并希望 Time 类可以访问 Date 类的私有成员。我们可以在 Time 类中声明 Date 为友元类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
using namespace std;

class Time; // 前向声明

class Date
{
public:
void printTime(const Time& t); // 声明函数,访问 Time 类的私有成员
};

class Time
{
private:
int hour;
int minute;
public:
Time(int h, int m) : hour(h), minute(m) {}
friend class Date; // 声明 Date 类为友元类
};

void Date::printTime(const Time& t)
{
cout << "Time: " << t.hour << ":" << t.minute << endl;
}

int main()
{
Time t(10, 30);
Date d;
d.printTime(t); // 输出:Time: 10:30
return 0;
}

在这个例子中,Time 类声明了 Date 类为友元类,这样 Date 类的成员函数 printTime 就能够访问 Time 类的私有成员(如 hourminute)。

SO:

  • 友元函数:允许非成员函数访问类的私有和保护成员。它不是类的成员函数,通常用于操作符重载等场景。
  • 友元类:允许一个类的所有成员函数访问另一个类的私有和保护成员。它们的关系是单向的。
  • 使用场景:友元可以提高代码的灵活性和可操作性,但过多使用会增加类之间的耦合度,破坏封装,因此要谨慎使用。

4. 内部类(Nested Class)

4.1 概念:

内部类 是定义在另一个类内部的类。它是一个独立的类,和外部类没有直接的归属关系,即它并不属于外部类的对象。外部类不能直接访问内部类的成员,反之,内部类可以访问外部类的成员,特别是当外部类的成员是 static 时(内部类天然是外部类的友元,可以访问外部类的私有成员)。

内部类 有时也被称为 嵌套类,通常用于将一个类作为一个辅助工具类嵌套在另一个类中,通常与外部类的功能紧密相关。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A
{
static int _count;
public:
class B
{
public:
void ShowCount()
{
cout << _count << endl; // 直接访问静态成员
}
};
};

int A::_count = 10;

int main()
{
A::B b;
b.ShowCount(); // 输出10
}

关键点:

  1. 独立性:内部类是一个完全独立的类,它不属于外部类的对象。因此,外部类不能通过自身的对象直接访问内部类的成员。
  2. 友元关系:虽然外部类不能直接访问内部类的成员,但内部类可以通过外部类的对象访问外部类的所有成员(包括私有成员)。从这个角度来看,内部类可以看作外部类的友元类(内部类天然具有可以访问外部类的私有成员这种访问权限,而不是因为它是友元类)。

4.2 内部类的特性

  1. 访问权限
    • 内部类可以定义在外部类的 publicprotectedprivate 等任何区域,和外部类的访问权限相同。
    • 但外部类不能直接访问内部类的成员,除非通过内部类的对象来访问。
    • 内部类可以访问外部类的非静态成员,但需要通过外部类的对象来访问。
  2. 访问外部类的静态成员
    • 内部类可以直接访问外部类中的 static 成员,无需外部类的对象或类名。这是因为静态成员是属于类本身的,而不是某个特定对象的。
  3. sizeof 外部类和内部类
    • 外部类和内部类是两个独立的实体,它们占用不同的内存空间。通过 sizeof 计算外部类的大小时,和内部类没有直接关系。

4.3 内部类的分类

  • 静态内部类static nested class):内部类是静态的,意味着它不需要外部类的实例就可以访问。静态内部类不能访问外部类的实例成员,但可以访问外部类的静态成员。
  • 非静态内部类(普通内部类):需要通过外部类的实例来创建实例,且能够访问外部类的所有成员(包括非静态成员)。

示例:内部类的使用

下面我们通过一个例子,详细讲解内部类的使用。

1. 普通的内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;

class Outer
{
private:
int outerVar = 10;
public:
class Inner
{
public:
void accessOuter(Outer& outer)
{
// 内部类可以访问外部类的私有成员
cout << "访问外部类 private 成员:" << outer.outerVar << endl;
}
};
};

int main()
{
Outer outer;
Outer::Inner inner; // 创建内部类对象
inner.accessOuter(outer); // 通过内部类访问外部类的成员
return 0;
}

输出:

1
访问外部类 private 成员:10

在这个例子中,InnerOuter 类的内部类。Inner 类可以访问 Outer 类的私有成员 outerVar,并且我们通过 Outer 类的对象来访问外部类的成员。

2. 静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;

class Outer
{
private:
static int staticVar; // 静态成员
public:
static class Inner
{
public:
void accessOuter()
{
// 静态内部类可以访问外部类的静态成员
cout << "访问外部类 static 成员:" << staticVar << endl;
}
};
};

int Outer::staticVar = 20; // 类外初始化静态成员变量

int main()
{
Outer::Inner inner; // 创建静态内部类对象
inner.accessOuter(); // 静态内部类访问外部类的静态成员
return 0;
}

输出:

1
访问外部类 static 成员:20

在这个例子中,InnerOuter 类的静态内部类。静态内部类可以直接访问 Outer 类的静态成员 staticVar,不需要外部类的对象。

3. 普通内部类和静态内部类的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
using namespace std;

class Outer
{
int outerVar = 10;
public:
class Inner
{
public:
void accessOuter(Outer& outer)
{
cout << "非静态内部类:" << outer.outerVar << endl;
}
};

static class StaticInner
{
public:
void accessOuter()
{
cout << "static 内部类:" << outerVar << endl; // 编译错误
}
};
};

int main()
{
Outer outer;
Outer::Inner inner;
inner.accessOuter(outer); // 非静态内部类访问外部类成员

Outer::StaticInner staticInner;
// staticInner.accessOuter(); // 编译错误

return 0;
}

SO:

  • 内部类 是定义在另一个类内部的类。它是独立的类,外部类不能直接访问内部类的成员,内部类可以通过外部类的对象访问外部类的所有成员。
  • 友元关系:从某种角度看,内部类可以访问外部类的私有成员,所以可以视作外部类的友元类。
  • 内部类可以是 静态的static),也可以是 非静态的,它们的访问权限和行为有所不同。

内部类通常用于处理一些与外部类紧密相关的功能,帮助将代码组织得更好。


5. 匿名对象(Anonymous Object)

在 C++中,匿名对象指的是没有明确名称的对象。它通常用于函数返回、临时数据传递、类型转换等场景,它们的生命周期仅限于它们所在的表达式或者函数调用,执行完毕后即被销毁。通过合理使用匿名对象,可以简化代码、减少不必要的对象创建,提高程序的效率。下面我将详细讲解匿名对象的相关知识点及其使用。

5.1 匿名对象的定义和创建

匿名对象是没有名字的临时对象。在 C++中,匿名对象通常出现在以下场景:

  • 作为函数返回值: 当函数返回一个对象时,C++会创建一个匿名对象来接收返回值。
  • 临时对象: 用作表达式的操作数时,编译器会创建一个临时对象。
  • 类型转换: 在类型转换过程中,C++会临时创建匿名对象。

5.2 匿名对象的生命周期

匿名对象的生命周期非常短,通常只在一个表达式或者函数调用期间有效。它们会在表达式结束后立即销毁。这是因为它们没有名字,无法直接引用它们。

5.3 匿名对象的使用示例

示例 1:作为函数返回值的匿名对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
using namespace std;

class MyClass
{
public:
MyClass()
{
cout << "MyClass 构造函数" << endl;
}

~MyClass()
{
cout << "MyClass 析构函数" << endl;
}

void sayHello()
{
cout << "Hello from MyClass!" << endl;
}
};

// 函数返回匿名对象
MyClass createObject()
{
return MyClass(); // 返回一个匿名对象
}

int main()
{
createObject().sayHello(); // 创建一个匿名对象并调用它的方法
return 0;
}

解释:

  • createObject 函数中,return MyClass(); 创建了一个匿名对象并返回。
  • main 函数中,调用 createObject().sayHello() 时,匿名对象在 createObject 函数返回时创建,并且调用 sayHello() 方法。
  • 程序运行时,首先会打印构造函数的消息,接着打印 sayHello() 的消息,然后销毁匿名对象,打印析构函数的消息。

示例 2:作为临时对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
using namespace std;

class MyClass
{
public:
MyClass()
{
cout << "MyClass 构造函数" << endl;
}

~MyClass()
{
cout << "MyClass 析构函数" << endl;
}

void sayHello()
{
cout << "Hello from MyClass!" << endl;
}
};

int main()
{
MyClass obj;
obj.sayHello();
MyClass().sayHello(); // 这里是一个匿名对象调用方法
return 0;
}

解释:

  • main 函数中,MyClass().sayHello(); 创建了一个匿名对象并调用了 sayHello 方法。
  • 这个匿名对象仅在该行代码执行时有效,执行完后立即销毁。

示例 3:通过类型转换创建匿名对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;

class MyClass
{
public:
MyClass(int value) : m_value(value)
{
cout << "MyClass 构造函数:" << m_value << endl;
}

~MyClass()
{
cout << "MyClass 析构函数" << endl;
}

private:
int m_value;
};

int main()
{
MyClass(10); // 创建一个匿名对象并传递给构造函数
return 0;
}

解释:

  • MyClass(10); 创建了一个匿名对象,并且传递了参数 10 给构造函数。
  • 这个匿名对象在创建后立即销毁,生命周期仅限于该行代码。

5.4 匿名对象的应用场景

匿名对象有很多实际应用,下面列出一些常见的场景:

  1. 临时数据传递: 在函数调用时传递临时对象,避免了不必要的对象复制。

    例如:

    1
    2
    3
    4
    5
    6
    void processObject(const MyClass& obj)
    {
    // 处理传入的对象
    }

    processObject(MyClass(5)); // 创建一个匿名对象并传递
  2. 简化代码: 当不需要重复使用对象时,可以通过匿名对象来简化代码,避免创建多余的变量。

  3. 链式调用: 匿名对象可以用于链式调用多个函数。

    例如:

    1
    MyClass().sayHello().anotherFunction();  // 链式调用匿名对象的方法

5.5 注意事项

  • 内存管理: 匿名对象通常是自动管理的,C++会在它们超出作用域后自动销毁。这意味着开发者不需要手动释放内存,但如果匿名对象涉及到动态内存分配(如 new),则需要特别注意内存管理。
  • 避免悬挂引用: 由于匿名对象的生命周期很短,必须避免在它销毁后访问它。

传道解惑

Q1:为什么匿名对象加 const 可以延长生命周期?

将匿名对象加上 const 修饰符,可以延长其生命周期。但这种延长的生命周期并不是无条件的,它的背后有一些特定的规则和原理。

在 C++中,匿名对象的生命周期是由它们的 作用域 决定的,通常在一个表达式或函数调用结束时,匿名对象会被销毁。但是,如果将匿名对象声明为 const 类型,它将与一个 引用 绑定,从而延长其生命周期。这是因为 const 引用允许我们在对象生命周期结束后,依然通过引用来使用它。

具体解释:

  1. 匿名对象与临时对象的生命周期:
  • 默认情况下,匿名对象(临时对象)的生命周期通常非常短,仅限于它的表达式或语句的结束。例如:

    1
    MyClass().doSomething();  // 匿名对象在 doSomething() 执行完后销毁
  1. 使用 const 引用延长生命周期:
  • 当匿名对象绑定到一个 const 引用时,C++会保证匿名对象的生命周期至少延长到该引用的生命周期结束。也就是说,这个引用会“延迟”对象销毁的时机,直到引用被销毁。
  • 关键点: const 引用可以延长临时对象的生命周期,使其存在于引用的作用域中,直到引用超出作用域。

示例:

1
2
3
4
5
6
7
8
9
10
11
void foo(const MyClass& obj)
{
obj.doSomething(); // obj 是对匿名对象的引用
}

int main()
{
foo(MyClass()); // 匿名对象绑定到 const 引用 obj
// 匿名对象在 foo() 返回时才销毁
return 0;
}

在这个例子中:

  • MyClass() 创建了一个匿名对象。
  • 这个匿名对象会被传递给 foo() 函数,并绑定到 const MyClass& obj 上。
  • 匿名对象的生命周期被延长,直到 obj 超出作用域,也就是 foo() 函数结束。

临时对象的绑定规则:

  • 当临时对象(匿名对象)被绑定到一个 const 引用时,C++会延长临时对象的生命周期,直到引用超出作用域。
  • 这样做的目的是为了避免因临时对象提前销毁而导致引用悬挂问题(即引用一个已销毁的临时对象)。

例子:匿名对象与 const 引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
>#include <iostream>
>using namespace std;

>class MyClass
>{
>public:
MyClass()
{
cout << "MyClass 构造函数" << endl;
}

~MyClass()
{
cout << "MyClass 析构函数" << endl;
}

void doSomething()
{
cout << "做点什么!" << endl;
}
>};

>void processObject(const MyClass& obj)
>{
obj.doSomething(); // 这里 obj 是对匿名对象的引用
>}

>int main()
>{
processObject(MyClass()); // 匿名对象绑定到 const 引用 obj
// 匿名对象的生命周期会被延长,直到 processObject 返回
return 0;
>}

输出:

1
2
3
>MyClass 构造函数
>做点什么!
>MyClass 析构函数

解释:

  • processObject 函数中,MyClass() 创建了一个匿名对象,它被传递并绑定到 const MyClass& obj
  • 由于 objconst 引用,匿名对象的生命周期被延长,直到 processObject 函数返回。
  • 匿名对象的析构函数只会在 processObject 函数结束后调用。

重要说明:

  • const 引用延长生命周期的作用范围: const 引用的作用是延长临时对象的生命周期,直到引用超出作用域。这意味着匿名对象在引用的作用域内存在,引用超出作用域后,匿名对象才会销毁。而这样做是为了避免临时对象在使用时被提前销毁,确保引用对象的有效性。这种机制是 C++中的一种特性,它通过引用的生命周期来保证匿名对象在函数作用域内的安全访问。
  • const 引用不允许绑定临时对象: 如果你尝试用一个非 const 引用绑定临时对象,C++ 编译器会报错,因为非 const 引用无法延长临时对象的生命周期。

Q2:匿名对象 VS 有名对象

匿名对象有名对象 在 C++中的最大区别在于命名、生命周期以及访问方式。匿名对象通常用于临时需要的场合,生命周期短,而有名对象则用于需要在多个地方访问和操作的情形,生命周期较长。下面我将详细解释:

命名

  • 匿名对象(Anonymous Object):顾名思义,它没有明确的名称,通常是临时创建的对象,不会绑定到变量上。

  • 例如:MyClass().doSomething();,在这里 MyClass() 创建了一个匿名对象,它没有名称,仅用于调用 doSomething() 方法。

  • 有名对象(Named Object):有明确的名称,可以通过变量名访问。

  • 例如:

    1
    2
    MyClass obj;  // obj 是有名对象
    obj.doSomething();

生命周期

  • 匿名对象:生命周期非常短暂,仅存在于当前表达式或者语句中,一旦使用完毕,匿名对象会被销毁。
  • 例如:MyClass().doSomething(); 中,匿名对象会在调用 doSomething() 后立刻销毁。
  • 有名对象:生命周期通常由它的作用域决定。对象在创建时分配内存,并在其作用域结束时被销毁。如果对象在栈上创建,它会在离开作用域时销毁;如果在堆上创建,则需要手动释放内存。
  • 例如:MyClass obj;obj 离开作用域时销毁。

访问方式

  • 匿名对象:不能通过变量名访问,因为它没有名称。只能在它创建的上下文中直接使用它。
  • 例如:MyClass().doSomething(); 中没有 MyClass 对象的名称,无法在之后访问它。
  • 有名对象:可以通过对象的名称来引用和访问对象的成员。
  • 例如:obj.doSomething(); 中,obj 是有名对象,可以在之后的代码中多次使用。

内存管理

  • 匿名对象:由于没有名称,它通常是栈上分配的,编译器在合适的时候自动管理内存。对于返回值优化(RVO/NRVO)等,编译器会优化创建匿名对象的内存管理,避免不必要的拷贝。
  • 有名对象:有名称,可以显式创建在栈上或堆上。栈上的对象在作用域结束时自动销毁,而堆上的对象则需要手动 delete

应用场景

匿名对象

  • 适用于 临时使用,例如一次性计算或者在函数调用中使用临时对象。

  • 常见于函数返回值、类型转换、临时数据传递等场景。

  • 示例:

    1
    2
    3
    4
    5
    6
    void processObject(MyClass obj)
    {
    obj.doSomething();
    }

    processObject(MyClass()); // 匿名对象作为参数传递

有名对象

  • 适用于 需要多次访问 的场景,或者需要在多个地方使用该对象。

  • 典型用法是作为类的实例,创建时需要明确的对象名来进行后续操作。

  • 示例:

    1
    2
    MyClass obj;  // 有名对象
    obj.doSomething(); // 可以在后续访问该对象

返回值优化(RVO/NRVO)

  • 匿名对象 在函数返回值时,编译器会尽可能优化,避免多余的拷贝操作,这被称为 返回值优化(RVO)或者 命名返回值优化(NRVO)。这意味着,返回匿名对象时,编译器会直接在调用位置构造返回对象,而不会创建临时对象。

  • 例如:

    1
    2
    3
    4
    MyClass createObject()
    {
    return MyClass(); // 匿名对象直接返回
    }
  • 有名对象 没有这样的优化问题,通常会被拷贝或者移动到调用处,特别是在涉及对象返回时。

性能差异

  • 匿名对象:由于其生命周期非常短,编译器有时能够优化它们的创建和销毁过程,避免不必要的复制。
  • 在某些场景中,匿名对象能避免额外的内存分配和释放开销,提升性能。
  • 有名对象:虽然生命周期较长,但如果不合理使用,有时会增加额外的开销,尤其是在传递大对象时,可能会发生不必要的拷贝操作。

总结

特性 匿名对象 有名对象
命名 没有名称,仅为临时对象 有名称,可以通过变量名访问
生命周期 短暂,仅在表达式或函数调用期间存在 生命周期由作用域决定,作用域结束时销毁
访问方式 不能直接访问,通常仅在当前表达式中使用 可以通过名称多次访问
内存管理 编译器自动管理内存,通常是栈上分配 可以是栈上或堆上,需要显式管理堆对象的内存
应用场景 临时数据传递、返回值、一次性计算等 需要多次使用、存储数据或状态等
性能差异 编译器优化可能避免不必要的复制 如果不小心使用,可能有不必要的拷贝操作### 总结

6. 再次理解类和对象

理解 对象 的概念,能帮助我们更好地理解面向对象编程(OOP)的核心思想。为了更通俗地讲解这个内容,我们可以通过一个现实中的例子来帮助理解。

1. 类是对事物的抽象

类就像是对某种 事物(例如洗衣机)的 抽象描述。它是 对现实中事物的建模,在程序中描述这个事物的 属性行为

  • 属性:就是这个事物的特征,比如洗衣机的品牌、颜色、容量等。
  • 行为:就是这个事物可以做的事情,比如洗衣机可以“启动”、“停止”、“洗衣服”等。

举个例子,洗衣机这个事物可以用类来描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class WashingMachine
{
public:
string brand; // 洗衣机的品牌
string color; // 洗衣机的颜色
int capacity; // 洗衣机的容量(比如5kg、10kg)

void start()
{
cout << "Washing machine started." << endl; // 启动洗衣机
}

void stop()
{
cout << "Washing machine stopped." << endl; // 停止洗衣机
}
};

在这个例子中,WashingMachine 类描述了一个洗衣机的 属性brand, color, capacity)和 行为start()stop())。这个类的作用就是 抽象化 洗衣机,将它的特征和行为描述给计算机。

2. 对象是类的实例化

类是对现实事物的抽象描述,但计算机无法直接“认识”类,必须通过 实例化 类来创建 对象,而对象才是计算机可以操作的具体实体。

  • 实例化:就是通过类创建具体的对象的过程。
  • 对象:是类的具体实例,表示现实世界中的某个具体的事物。比如,你可以通过 WashingMachine 类创建多个洗衣机对象,每个对象代表一个具体的洗衣机。

例子继续,假设我们现在创建了一个洗衣机对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
using namespace std;

class WashingMachine
{
public:
string brand; // 洗衣机的品牌
string color; // 洗衣机的颜色
int capacity; // 洗衣机的容量(单位:kg)

void start()
{
cout << "Washing machine started." << endl;
}

void stop()
{
cout << "Washing machine stopped." << endl;
}

void wash()
{
cout << "Washing clothes..." << endl;
}
};

int main()
{
WashingMachine wm1;
wm1.brand = "小米";
wm1.color = "白色";
wm1.capacity = 10;

wm1.start(); // 启动洗衣机
wm1.wash(); // 洗衣服
wm1.stop(); // 停止洗衣机

WashingMachine wm2;
wm2.brand = "格力";
wm2.color = "黑色";
wm2.capacity = 8;

wm2.start();
wm2.wash();
wm2.stop();
return 0;
}

在这个例子中,wm1wm2WashingMachine 类的两个 对象。它们分别代表两个不同的洗衣机,每个对象的属性(brand, color, capacity)可以有不同的值。通过这些对象,我们可以模拟现实中的多个洗衣机。

3. 通过类创建对象

从以上的例子可以看出,类只是描述了洗衣机的属性和行为,而对象才是 具体的实例。你可以通过类创建出多个对象,每个对象都代表一个具体的事物。类就像是一个模板或蓝图,具体的对象是根据这个模板生成的。

4. 类和对象的关系

  • 是对 事物 的一种描述,它定义了这个事物的 属性行为
  • 对象 是类的 实例,是计算机可以直接操作的具体实体。通过类可以创建多个对象,每个对象都有不同的属性值和方法。

总结一下,类和对象的关系可以类比为:

  • :就像是一本 描述洗衣机的说明书,它告诉我们洗衣机有哪些属性(品牌、颜色、容量)和行为(启动、停止)。
  • 对象:就像是根据这本说明书实际生产出来的 具体洗衣机。每一台洗衣机都有自己的品牌、颜色、容量等信息,并可以执行启动、停止等操作。

5. 现实中的例子:洗衣机类

让我们通过现实中的洗衣机来进一步理解。

  1. 抽象洗衣机:当我们想到洗衣机时,我们并不会想到具体某一台洗衣机,而是先想到了“洗衣机”这个概念。它有品牌、颜色、容量这些特征,并且有启动、停止这些操作。这就是 的作用:把这些共性的特征和行为总结出来。
  2. 创建洗衣机对象:当你去买洗衣机时,你选择了一个品牌、颜色、容量等具体参数的洗衣机。每一台洗衣机就是一个 对象,它是类的实例化。
  3. 操作洗衣机:当你开始使用这台洗衣机时,你可以通过按按钮来“启动”和“停止”,这就是对象通过类提供的操作(方法)来实现的行为。

SO:

  • 是对现实世界中事物的抽象描述,它总结了事物的 属性行为
  • 对象 是类的具体实例,是计算机能够识别和操作的实体。
  • 是对事物的抽象描述,而 对象 是根据类创建的具体实例。
  • 通过 ,我们可以创建多个不同的 对象,每个对象具有类中定义的属性和行为。

通过理解类和对象的关系,你将能够更好地理解面向对象编程(OOP)的核心思想,这对于学习和使用 C++、Java 等面向对象语言非常重要。